home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / pyshared / apport / report.py < prev    next >
Encoding:
Python Source  |  2009-04-06  |  78.1 KB  |  2,092 lines

  1. '''Class for an apport report with some useful methods to collect standard
  2. debug information.
  3.  
  4. Copyright (C) 2006 Canonical Ltd.
  5. Author: Martin Pitt <martin.pitt@ubuntu.com>
  6.  
  7. This program is free software; you can redistribute it and/or modify it
  8. under the terms of the GNU General Public License as published by the
  9. Free Software Foundation; either version 2 of the License, or (at your
  10. option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
  11. the full text of the license.
  12. '''
  13.  
  14. import subprocess, tempfile, os.path, urllib, re, pwd, grp, os, sys
  15. import fnmatch, glob, atexit, traceback
  16.  
  17. import xml.dom, xml.dom.minidom
  18. from xml.parsers.expat import ExpatError
  19.  
  20. from problem_report import ProblemReport
  21. import fileutils
  22. from packaging_impl import impl as packaging
  23.  
  24. _hook_dir = '/usr/share/apport/package-hooks/'
  25. _common_hook_dir = '/usr/share/apport/general-hooks/'
  26.  
  27. # path of the ignore file
  28. _ignore_file = '~/.apport-ignore.xml'
  29.  
  30. # system-wide blacklist
  31. _blacklist_dir = '/etc/apport/blacklist.d'
  32.  
  33. # programs that we consider interpreters
  34. interpreters = ['sh', 'bash', 'dash', 'csh', 'tcsh', 'python*',
  35.     'ruby*', 'php', 'perl*', 'mono*', 'awk']
  36.  
  37. #
  38. # helper functions
  39. #
  40.  
  41. def _transitive_dependencies(package, depends_set):
  42.     '''Recursively add dependencies of package to depends_set.'''
  43.  
  44.     try:
  45.         cur_ver = packaging.get_version(package)
  46.     except ValueError:
  47.         return
  48.     for d in packaging.get_dependencies(package):
  49.         if not d in depends_set:
  50.             depends_set.add(d)
  51.             _transitive_dependencies(d, depends_set)
  52.  
  53. def _read_file(f):
  54.     '''Try to read given file and return its contents, or return a textual
  55.     error if it failed.'''
  56.  
  57.     try:
  58.         return open(f).read().strip()
  59.     except (OSError, IOError), e:
  60.         return 'Error: ' + str(e)
  61.  
  62. def _read_maps(pid):
  63.     '''
  64.     Since /proc/$pid/maps may become unreadable unless we are
  65.     ptracing the process, detect this, and attempt to attach/detach
  66.     '''
  67.  
  68.     maps = 'Error: unable to read /proc maps file'
  69.     try:
  70.         maps = file('/proc/%d/maps' % pid).read().strip()
  71.     except (OSError,IOError), e:
  72.         return 'Error: ' + str(e)
  73.     return maps
  74.  
  75. def _command_output(command, input = None, stderr = subprocess.STDOUT):
  76.     '''Try to execute given command (array) and return its stdout, or return
  77.     a textual error if it failed.'''
  78.  
  79.     sp = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=stderr, close_fds=True)
  80.  
  81.     (out, err) = sp.communicate(input)
  82.     if sp.returncode == 0:
  83.         return out
  84.     else:
  85.        raise OSError, 'Error: command %s failed with exit code %i: %s' % (
  86.            str(command), sp.returncode, err)
  87.  
  88. def _check_bug_pattern(report, pattern):
  89.     '''Check if given report matches the given bug pattern XML DOM node; return the
  90.     bug URL on match, otherwise None.'''
  91.  
  92.     if not pattern.attributes.has_key('url'):
  93.         return None
  94.  
  95.     for c in pattern.childNodes:
  96.         # regular expression condition
  97.         if c.nodeType == xml.dom.Node.ELEMENT_NODE and c.nodeName == 're' and \
  98.             c.attributes.has_key('key'):
  99.             key = c.attributes['key'].nodeValue
  100.             if not report.has_key(key):
  101.                 return None
  102.             c.normalize()
  103.             if c.hasChildNodes() and \
  104.                 c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE:
  105.                 regexp = c.childNodes[0].nodeValue.encode('UTF-8')
  106.                 try:
  107.                     if not re.search(regexp, report[key]):
  108.                         return None
  109.                 except:
  110.                     return None
  111.  
  112.     return pattern.attributes['url'].nodeValue.encode('UTF-8')
  113.  
  114. def _dom_remove_space(node):
  115.     '''Recursively remove whitespace from given XML DOM node.'''
  116.  
  117.     for c in node.childNodes:
  118.         if c.nodeType == xml.dom.Node.TEXT_NODE and c.nodeValue.strip() == '':
  119.             c.unlink()
  120.             node.removeChild(c)
  121.         else:
  122.             _dom_remove_space(c)
  123.  
  124. def get_module_license(module):
  125.     '''Return the license for a given kernel module.'''
  126.  
  127.     try:
  128.         modinfo = subprocess.Popen(['/sbin/modinfo', module], stdout=subprocess.PIPE)
  129.         out = modinfo.communicate()[0]
  130.         if modinfo.returncode != 0:
  131.             return None
  132.     except OSError:
  133.         return None
  134.     for l in out.splitlines():
  135.         fields = l.split(':', 1)
  136.         if len(fields) < 2:
  137.             continue
  138.         if fields[0] == 'license':
  139.             return fields[1].strip()
  140.  
  141.     return None
  142.  
  143. def nonfree_modules(module_list = '/proc/modules'):
  144.     '''Check loaded modules and return a list of those which are not free.'''
  145.     try:
  146.         mods = [l.split()[0] for l in open(module_list)]
  147.     except IOError:
  148.         return []
  149.  
  150.     nonfree = []
  151.     for m in mods:
  152.         l = get_module_license(m)
  153.         if l and not ('GPL' in l or 'BSD' in l or 'MPL' in l or 'MIT' in l):
  154.             nonfree.append(m)
  155.  
  156.     return nonfree
  157.  
  158. #
  159. # Report class
  160. #
  161.  
  162. class Report(ProblemReport):
  163.     '''A problem report specific to apport (crash or bug).
  164.  
  165.     This class wraps a standard ProblemReport and adds methods for collecting
  166.     standard debugging data.'''
  167.  
  168.     def __init__(self, type='Crash', date=None):
  169.         '''Initialize a fresh problem report.
  170.  
  171.            date is the desired date/time string; if None (default), the current
  172.            local time is used.
  173.            '''
  174.  
  175.         ProblemReport.__init__(self, type, date)
  176.  
  177.     def _pkg_modified_suffix(self, package):
  178.         '''Return a string suitable for appending to Package:/Dependencies:
  179.         fields.
  180.  
  181.         If package has only unmodified files, return the empty string. If not,
  182.         return ' [modified: ...]' with a list of modified files.'''
  183.  
  184.         mod = packaging.get_modified_files(package)
  185.         if mod:
  186.             return ' [modified: %s]' % ' '.join(mod)
  187.         else:
  188.             return ''
  189.  
  190.     def add_package_info(self, package = None):
  191.         '''Add packaging information.
  192.  
  193.         If package is not given, the report must have ExecutablePath.
  194.         This adds:
  195.         - Package: package name and installed version
  196.         - SourcePackage: source package name
  197.         - PackageArchitecture: processor architecture this package was built
  198.           for
  199.         - Dependencies: package names and versions of all dependencies and
  200.           pre-dependencies; this also checks if the files are unmodified and
  201.           appends a list of all modified files'''
  202.  
  203.         if not package:
  204.             package = fileutils.find_file_package(self['ExecutablePath'])
  205.             if not package:
  206.                 return
  207.  
  208.         self['Package'] = '%s %s%s' % (package,
  209.             packaging.get_version(package),
  210.             self._pkg_modified_suffix(package))
  211.         self['SourcePackage'] = packaging.get_source(package)
  212.         self['PackageArchitecture'] = packaging.get_architecture(package)
  213.  
  214.         # get set of all transitive dependencies
  215.         dependencies = set([])
  216.         _transitive_dependencies(package, dependencies)
  217.  
  218.         # get dependency versions
  219.         self['Dependencies'] = ''
  220.         for dep in dependencies:
  221.             try:
  222.                 v = packaging.get_version(dep)
  223.             except ValueError:
  224.                 # can happen with uninstalled alternate dependencies
  225.                 continue
  226.  
  227.             if self['Dependencies']:
  228.                 self['Dependencies'] += '\n'
  229.             self['Dependencies'] += '%s %s%s' % (dep, v,
  230.                 self._pkg_modified_suffix(dep))
  231.  
  232.     def add_os_info(self):
  233.         '''Add operating system information.
  234.  
  235.         This adds:
  236.         - DistroRelease: lsb_release -sir output
  237.         - Architecture: system architecture in distro specific notation
  238.         - Uname: uname -srm output
  239.         - NonfreeKernelModules: loaded kernel modules which are not free (if
  240.             there are none, this field will not be present)'''
  241.  
  242.         p = subprocess.Popen(['lsb_release', '-sir'], stdout=subprocess.PIPE,
  243.             stderr=subprocess.PIPE, close_fds=True)
  244.         self['DistroRelease'] = p.communicate()[0].strip().replace('\n', ' ')
  245.  
  246.         u = os.uname()
  247.         self['Uname'] = '%s %s %s' % (u[0], u[2], u[4])
  248.         self['Architecture'] = packaging.get_system_architecture()
  249.         nm = nonfree_modules()
  250.         if nm:
  251.             self['NonfreeKernelModules'] = ' '.join(nonfree_modules())
  252.  
  253.     def add_user_info(self):
  254.         '''Add information about the user.
  255.  
  256.         This adds:
  257.         - UserGroups: system groups the user is in
  258.         '''
  259.  
  260.         user = pwd.getpwuid(os.getuid()).pw_name
  261.         groups = [name for name, p, gid, memb in grp.getgrall()
  262.             if user in memb and gid < 1000]
  263.         groups.sort()
  264.         self['UserGroups'] = ' '.join(groups)
  265.  
  266.     def _check_interpreted(self):
  267.         '''Check ExecutablePath, ProcStatus and ProcCmdline if the process is
  268.         interpreted.'''
  269.  
  270.         if not self.has_key('ExecutablePath'):
  271.             return
  272.  
  273.         exebasename = os.path.basename(self['ExecutablePath'])
  274.  
  275.         # check if we consider ExecutablePath an interpreter; we have to do
  276.         # this, otherwise 'gedit /tmp/foo.txt' would be detected as interpreted
  277.         # script as well
  278.         if not filter(lambda i: fnmatch.fnmatch(exebasename, i), interpreters):
  279.             return
  280.  
  281.         # first, determine process name
  282.         name = None
  283.         for l in self['ProcStatus'].splitlines():
  284.             try:
  285.                 (k, v) = l.split('\t', 1)
  286.             except ValueError:
  287.                 continue
  288.             if k == 'Name:':
  289.                 name = v
  290.                 break
  291.         if not name:
  292.             return
  293.  
  294.         cmdargs = self['ProcCmdline'].split('\0')
  295.         bindirs = ['/bin/', '/sbin/', '/usr/bin/', '/usr/sbin/']
  296.  
  297.         # filter out interpreter options
  298.         while len(cmdargs) >= 2 and cmdargs[1].startswith('-'):
  299.             del cmdargs[1]
  300.  
  301.         # catch scripts explicitly called with interpreter
  302.         if len(cmdargs) >= 2:
  303.             # ensure that cmdargs[1] is an absolute path
  304.             if cmdargs[1].startswith('.') and self.has_key('ProcCwd'):
  305.                 cmdargs[1] = os.path.join(self['ProcCwd'], cmdargs[1])
  306.             if os.access(cmdargs[1], os.R_OK):
  307.                 self['InterpreterPath'] = self['ExecutablePath']
  308.                 self['ExecutablePath'] = os.path.realpath(cmdargs[1])
  309.                 return
  310.  
  311.         # catch directly executed scripts
  312.         if name != exebasename:
  313.             argvexes = filter(lambda p: os.access(p, os.R_OK), [p+cmdargs[0] for p in bindirs])
  314.             if argvexes and os.path.basename(os.path.realpath(argvexes[0])) == name:
  315.                 self['InterpreterPath'] = self['ExecutablePath']
  316.                 self['ExecutablePath'] = argvexes[0]
  317.                 return
  318.  
  319.     def add_proc_info(self, pid=None, extraenv=[]):
  320.         '''Add /proc/pid information.
  321.  
  322.         If pid is not given, it defaults to the process' current pid.
  323.  
  324.         This adds the following fields:
  325.         - ExecutablePath: /proc/pid/exe contents; if the crashed process is
  326.           interpreted, this contains the script path instead
  327.         - InterpreterPath: /proc/pid/exe contents if the crashed process is
  328.           interpreted; otherwise this key does not exist
  329.         - ProcEnviron: A subset of the process' environment (only some standard
  330.           variables that do not disclose potentially sensitive information, plus
  331.           the ones mentioned in extraenv)
  332.         - ProcCmdline: /proc/pid/cmdline contents
  333.         - ProcStatus: /proc/pid/status contents
  334.         - ProcMaps: /proc/pid/maps contents
  335.         - ProcAttrCurrent: /proc/pid/attr/current contents'''
  336.  
  337.         if not pid:
  338.             pid = os.getpid()
  339.         pid = str(pid)
  340.  
  341.         try:
  342.             self['ProcCwd'] = os.readlink('/proc/' + pid + '/cwd')
  343.         except OSError:
  344.             pass
  345.         self.add_proc_environ(pid, extraenv)
  346.         self['ProcStatus'] = _read_file('/proc/' + pid + '/status')
  347.         self['ProcCmdline'] = _read_file('/proc/' + pid + '/cmdline').rstrip('\0')
  348.         self['ProcMaps'] = _read_maps(int(pid))
  349.         self['ExecutablePath'] = os.readlink('/proc/' + pid + '/exe')
  350.         for p in ('rofs', 'rwfs', 'squashmnt', 'persistmnt'):
  351.             if self['ExecutablePath'].startswith('/%s/' % p):
  352.                 self['ExecutablePath'] = self['ExecutablePath'][len('/%s' % p):]
  353.                 break
  354.         assert os.path.exists(self['ExecutablePath'])
  355.  
  356.         # check if we have an interpreted program
  357.         self._check_interpreted()
  358.  
  359.         # make ProcCmdline ASCII friendly, do shell escaping
  360.         self['ProcCmdline'] = self['ProcCmdline'].replace('\\', '\\\\').replace(' ', '\\ ').replace('\0', ' ')
  361.  
  362.         # grab AppArmor or SELinux context
  363.         # If no LSM is loaded, reading will return -EINVAL
  364.         try:
  365.             # On Linux 2.6.28+, 'current' is world readable, but read() gives
  366.             # EPERM; Python 2.5.3+ crashes on that (LP: #314065)
  367.             if os.getuid() == 0:
  368.                 self['ProcAttrCurrent'] = open('/proc/' + pid + '/attr/current').read().strip()
  369.         except (IOError, OSError):
  370.             pass
  371.  
  372.     def add_proc_environ(self, pid=None, extraenv=[]):
  373.         '''Add environment information.
  374.  
  375.         If pid is not given, it defaults to the process' current pid.
  376.  
  377.         This adds the following fields:
  378.         - ProcEnviron: A subset of the process' environment (only some standard
  379.           variables that do not disclose potentially sensitive information, plus
  380.           the ones mentioned in extraenv)
  381.         '''
  382.         safe_vars = ['SHELL', 'LANGUAGE', 'LANG', 'LC_CTYPE',
  383.             'LC_COLLATE', 'LC_TIME', 'LC_NUMERIC', 'LC_MONETARY', 'LC_MESSAGES',
  384.             'LC_PAPER', 'LC_NAME', 'LC_ADDRESS', 'LC_TELEPHONE', 'LC_MEASUREMENT',
  385.             'LC_IDENTIFICATION', 'LOCPATH'] + extraenv
  386.  
  387.         if not pid:
  388.             pid = os.getpid()
  389.         pid = str(pid)
  390.  
  391.         self['ProcEnviron'] = ''
  392.         env = _read_file('/proc/'+ pid + '/environ').replace('\n', '\\n')
  393.         if env.startswith('Error:'):
  394.             self['ProcEnviron'] = env
  395.         else:
  396.             for l in env.split('\0'):
  397.                 if l.split('=', 1)[0] in safe_vars:
  398.                     if self['ProcEnviron']:
  399.                         self['ProcEnviron'] += '\n'
  400.                     self['ProcEnviron'] += l
  401.                 elif l.startswith('PATH='):
  402.                     p = l.split('=', 1)[1]
  403.                     if '/home' in p or '/tmp' in p:
  404.                         if self['ProcEnviron']:
  405.                             self['ProcEnviron'] += '\n'
  406.                         self['ProcEnviron'] += 'PATH=(custom, user)'
  407.                     elif p != '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games':
  408.                         if self['ProcEnviron']:
  409.                             self['ProcEnviron'] += '\n'
  410.                         self['ProcEnviron'] += 'PATH=(custom, no user)'
  411.  
  412.     def add_gdb_info(self, debugdir=None):
  413.         '''Add information from gdb.
  414.  
  415.         This requires that the report has a CoreDump and an
  416.         ExecutablePath. This adds the following fields:
  417.         - Registers: Output of gdb's 'info registers' command
  418.         - Disassembly: Output of gdb's 'x/16i $pc' command
  419.         - Stacktrace: Output of gdb's 'bt full' command
  420.         - ThreadStacktrace: Output of gdb's 'thread apply all bt full' command
  421.         - StacktraceTop: simplified stacktrace (topmost 5 functions) for inline
  422.           inclusion into bug reports and easier processing
  423.  
  424.         The optional debugdir can specify an alternative debug symbol root
  425.         directory.
  426.         '''
  427.  
  428.         if not self.has_key('CoreDump') or not self.has_key('ExecutablePath'):
  429.             return
  430.  
  431.         unlink_core = False
  432.         try:
  433.             if hasattr(self['CoreDump'], 'find'):
  434.                 (fd, core) = tempfile.mkstemp()
  435.                 os.write(fd, self['CoreDump'])
  436.                 os.close(fd)
  437.                 unlink_core = True
  438.             elif hasattr(self['CoreDump'], 'gzipvalue'):
  439.                 (fd, core) = tempfile.mkstemp()
  440.                 os.close(fd)
  441.                 self['CoreDump'].write(open(core, 'w'))
  442.                 unlink_core = True
  443.             else:
  444.                 core = self['CoreDump'][0]
  445.  
  446.             gdb_reports = {
  447.                            'Registers': 'info registers',
  448.                            'Disassembly': 'x/16i $pc',
  449.                            'Stacktrace': 'bt full',
  450.                            'ThreadStacktrace': 'thread apply all bt full',
  451.                           }
  452.  
  453.             command = ['gdb', '--batch']
  454.             if debugdir:
  455.                 command += ['--ex', 'set debug-file-directory ' + debugdir]
  456.             command += ['--ex', 'file ' + self.get('InterpreterPath',
  457.                 self['ExecutablePath']), '--ex', 'core-file ' + core]
  458.             # limit maximum backtrace depth (to avoid looped stacks)
  459.             command += ['--ex', 'set backtrace limit 2000']
  460.             value_keys = []
  461.             # append the actual commands and something that acts as a separator
  462.             for name, cmd in gdb_reports.iteritems():
  463.                 value_keys.append(name)
  464.                 command += ['--ex', 'p -99', '--ex', cmd]
  465.  
  466.             assert os.path.exists(self.get('InterpreterPath', self['ExecutablePath']))
  467.  
  468.             # call gdb
  469.             try:
  470.                 out = _command_output(command, stderr=open('/dev/null')).replace(
  471.                     '(no debugging symbols found)\n','').replace(
  472.                     'No symbol table info available.\n','')
  473.             except OSError:
  474.                 return
  475.  
  476.             # split the output into the various fields
  477.             part_re = re.compile('^\$\d+\s*=\s*-99$', re.MULTILINE)
  478.             parts = part_re.split(out)
  479.             # drop the gdb startup text prior to first separator
  480.             parts.pop(0)
  481.             for part in parts:
  482.                 self[value_keys.pop(0)] = part.replace('\n\n', '\n.\n').strip()
  483.         finally:
  484.             if unlink_core:
  485.                 os.unlink(core)
  486.  
  487.         if self.has_key('Stacktrace'):
  488.             self._gen_stacktrace_top()
  489.  
  490.     def _gen_stacktrace_top(self):
  491.         '''Build field StacktraceTop as the top five functions of Stacktrace. 
  492.  
  493.         Signal handler invocations and related functions are skipped since they
  494.         are generally not useful for triaging and duplicate detection.'''
  495.         
  496.         unwind_functions = set(['g_logv', 'g_log', 'IA__g_log', 'IA__g_logv',
  497.             'g_assert_warning', 'IA__g_assert_warning'])
  498.         toptrace = [''] * 5
  499.         depth = 0
  500.         unwound = False
  501.         unwinding = False
  502.         bt_fn_re = re.compile('^#(\d+)\s+(?:0x(?:\w+)\s+in\s+(.*)|(<signal handler called>)\s*)$')
  503.         bt_fn_noaddr_re = re.compile('^#(\d+)\s+(?:(.*)|(<signal handler called>)\s*)$')
  504.  
  505.         for line in self['Stacktrace'].splitlines():
  506.             m = bt_fn_re.match(line)
  507.             if not m:
  508.                 m = bt_fn_noaddr_re.match(line)
  509.                 if not m:
  510.                     continue
  511.  
  512.             if not unwound or unwinding:
  513.                 if m.group(2):
  514.                     fn = m.group(2).split()[0].split('(')[0]
  515.                 else:
  516.                     fn = None
  517.                 if m.group(3) or fn in unwind_functions:
  518.                     unwinding = True
  519.                     depth = 0
  520.                     toptrace = [''] * 5
  521.                     unwound = True
  522.                     continue
  523.                 else:
  524.                     unwinding = False
  525.  
  526.             if depth < len(toptrace):
  527.                 toptrace[depth] = m.group(2) or m.group(3)
  528.                 depth += 1
  529.         self['StacktraceTop'] = '\n'.join(toptrace).strip()
  530.  
  531.     def add_hooks_info(self):
  532.         '''Check for an existing hook script and run it to add additional
  533.         package specific information.
  534.  
  535.         A hook script needs to be in _hook_dir/<Package>.py or in
  536.         _common_hook_dir/*.py and has to contain a function 'add_info(report)'
  537.         that takes and modifies a Report.'''
  538.  
  539.         if 'Package' not in self:
  540.             return
  541.         symb = {}
  542.  
  543.         # common hooks
  544.         for hook in glob.glob(_common_hook_dir + '/*.py'):
  545.             try:
  546.                 execfile(hook, symb)
  547.                 symb['add_info'](self)
  548.             except:
  549.                 print >> sys.stderr, 'hook %s crashed:' % hook
  550.                 traceback.print_exc()
  551.                 pass
  552.  
  553.         # binary package hook
  554.         hook = '%s/%s.py' % (_hook_dir, self['Package'].split()[0])
  555.         if os.path.exists(hook):
  556.             try:
  557.                 execfile(hook, symb)
  558.                 symb['add_info'](self)
  559.             except:
  560.                 print >> sys.stderr, 'hook %s crashed:' % hook
  561.                 traceback.print_exc()
  562.                 pass
  563.  
  564.         # source package hook
  565.         if self.has_key('SourcePackage'):
  566.             hook = '%s/source_%s.py' % (_hook_dir, self['SourcePackage'].split()[0])
  567.             if os.path.exists(hook):
  568.                 try:
  569.                     execfile(hook, symb)
  570.                     symb['add_info'](self)
  571.                 except:
  572.                     print >> sys.stderr, 'hook %s crashed:' % hook
  573.                     traceback.print_exc()
  574.                     pass
  575.  
  576.     def search_bug_patterns(self, baseurl):
  577.         '''Check bug patterns at baseurl/packagename.xml, return bug URL on match or
  578.         None otherwise.
  579.  
  580.         The pattern file must be valid XML and has the following syntax:
  581.         root element := <patterns>
  582.         patterns := <pattern url="http://bug.url"> *
  583.         pattern := <re key="report_key">regular expression*</re> +
  584.  
  585.         For example:
  586.         <?xml version="1.0"?>
  587.         <patterns>
  588.             <pattern url="http://bugtracker.net/bugs/1">
  589.                 <re key="Foo">ba.*r</re>
  590.             </pattern>
  591.             <pattern url="http://bugtracker.net/bugs/2">
  592.                 <re key="Foo">write_(hello|goodbye)</re>
  593.                 <re key="Package">^\S* 1-2$</re> <!-- test for a particular version -->
  594.             </pattern>
  595.         </patterns>
  596.         '''
  597.  
  598.         # some distros might not want to support these
  599.         if not baseurl:
  600.             return
  601.  
  602.         assert self.has_key('Package')
  603.         package = self['Package'].split()[0]
  604.         try:
  605.             patterns = urllib.urlopen('%s/%s.xml' % (baseurl, package)).read()
  606.             assert '<title>404 Not Found' not in patterns
  607.         except:
  608.             # try if there is one for the source package
  609.             if self.has_key('SourcePackage'):
  610.                 try:
  611.                     patterns = urllib.urlopen('%s/%s.xml' % (baseurl, self['SourcePackage'])).read()
  612.                 except:
  613.                     return None
  614.             else:
  615.                 return None
  616.  
  617.         try:
  618.             dom = xml.dom.minidom.parseString(patterns)
  619.         except ExpatError:
  620.             return None
  621.  
  622.         for pattern in dom.getElementsByTagName('pattern'):
  623.             m = _check_bug_pattern(self, pattern)
  624.             if m:
  625.                 return m
  626.  
  627.         return None
  628.  
  629.     def _get_ignore_dom(self):
  630.         '''Read ignore list XML file and return a DOM tree, or an empty DOM
  631.         tree if file does not exist.
  632.  
  633.         Raises ValueError if the file exists but is invalid XML.'''
  634.  
  635.         ifpath = os.path.expanduser(_ignore_file)
  636.         if not os.access(ifpath, os.R_OK) or os.path.getsize(ifpath) == 0:
  637.             # create a document from scratch
  638.             dom = xml.dom.getDOMImplementation().createDocument(None, 'apport', None)
  639.         else:
  640.             try:
  641.                 dom = xml.dom.minidom.parse(ifpath)
  642.             except ExpatError, e:
  643.                 raise ValueError, '%s has invalid format: %s' % (_ignore_file, str(e))
  644.  
  645.         # remove whitespace so that writing back the XML does not accumulate
  646.         # whitespace
  647.         dom.documentElement.normalize()
  648.         _dom_remove_space(dom.documentElement)
  649.  
  650.         return dom
  651.  
  652.     def check_ignored(self):
  653.         '''Check ~/.apport-ignore.xml (in the real UID's home) and
  654.         /etc/apport/blacklist.d/ if the current report should not be presented
  655.         to the user.
  656.  
  657.         This requires the ExecutablePath attribute. Function can throw a
  658.         ValueError if the file has an invalid format.'''
  659.  
  660.         assert self.has_key('ExecutablePath')
  661.  
  662.         # check blacklist
  663.         try:
  664.             for f in os.listdir(_blacklist_dir):
  665.                 try:
  666.                     fd = open(os.path.join(_blacklist_dir, f))
  667.                 except IOError:
  668.                     continue
  669.                 for line in fd:
  670.                     if line.strip() == self['ExecutablePath']:
  671.                         return True
  672.         except OSError:
  673.             pass
  674.  
  675.         dom = self._get_ignore_dom()
  676.  
  677.         try:
  678.             cur_mtime = int(os.stat(self['ExecutablePath']).st_mtime)
  679.         except OSError:
  680.             # if it does not exist any more, do nothing
  681.             return False
  682.  
  683.         # search for existing entry and update it
  684.         for ignore in dom.getElementsByTagName('ignore'):
  685.             if ignore.getAttribute('program') == self['ExecutablePath']:
  686.                 if float(ignore.getAttribute('mtime')) >= cur_mtime:
  687.                     return True
  688.  
  689.         return False
  690.  
  691.     def mark_ignore(self):
  692.         '''Add a ignore list entry for this report to ~/.apport-ignore.xml, so
  693.         that future reports for this ExecutablePath are not presented to the
  694.         user any more.
  695.  
  696.         Function can throw a ValueError if the file already exists and has an
  697.         invalid format.'''
  698.  
  699.         assert self.has_key('ExecutablePath')
  700.  
  701.         dom = self._get_ignore_dom()
  702.         mtime = str(int(os.stat(self['ExecutablePath']).st_mtime))
  703.  
  704.         # search for existing entry and update it
  705.         for ignore in dom.getElementsByTagName('ignore'):
  706.             if ignore.getAttribute('program') == self['ExecutablePath']:
  707.                 ignore.setAttribute('mtime', mtime)
  708.                 break
  709.         else:
  710.             # none exists yet, create new ignore node if none exists yet
  711.             e = dom.createElement('ignore')
  712.             e.setAttribute('program', self['ExecutablePath'])
  713.             e.setAttribute('mtime', mtime)
  714.             dom.documentElement.appendChild(e)
  715.  
  716.         # write back file
  717.         dom.writexml(open(os.path.expanduser(_ignore_file), 'w'),
  718.             addindent='  ', newl='\n')
  719.  
  720.         dom.unlink()
  721.  
  722.     def has_useful_stacktrace(self):
  723.         '''Check whether this report has a stacktrace that can be considered
  724.         'useful'.
  725.  
  726.         The current heuristic is to consider it useless if it either is shorter
  727.         than three lines and has any unknown function, or for longer traces, a
  728.         minority of known functions.'''
  729.         
  730.         if not self.get('StacktraceTop'):
  731.             return False
  732.         
  733.         unknown_fn = [f.startswith('??') for f in self['StacktraceTop'].splitlines()]
  734.  
  735.         if len(unknown_fn) < 3:
  736.             return unknown_fn.count(True) == 0
  737.  
  738.         return unknown_fn.count(True) <= len(unknown_fn)/2.
  739.  
  740.     def standard_title(self):
  741.         '''Create an appropriate title for a crash database entry.
  742.  
  743.         This contains the topmost function name from the stack trace and the
  744.         signal (for signal crashes) or the Python exception (for unhandled
  745.         Python exceptions).
  746.  
  747.         Return None if the report is not a crash or a default title could not
  748.         be generated.'''
  749.  
  750.         # signal crash
  751.         if self.has_key('Signal') and \
  752.             self.has_key('ExecutablePath') and \
  753.             self.has_key('StacktraceTop'):
  754.  
  755.             signal_names = {
  756.                 '4': 'SIGILL',
  757.                 '6': 'SIGABRT',
  758.                 '8': 'SIGFPE',
  759.                 '11': 'SIGSEGV',
  760.                 '13': 'SIGPIPE'
  761.             }
  762.  
  763.             fn = ''
  764.             for l in self['StacktraceTop'].splitlines():
  765.                 fname = l.split('(')[0].strip()
  766.                 if fname != '??':
  767.                     fn = ' in %s()' % fname
  768.                     break
  769.  
  770.             arch_mismatch = ''
  771.             if self.has_key('Architecture') and \
  772.                 self.has_key('PackageArchitecture') and \
  773.                 self['Architecture'] != self['PackageArchitecture'] and \
  774.                 self['PackageArchitecture'] != 'all':
  775.                 arch_mismatch = ' [non-native %s package]' % self['PackageArchitecture']
  776.  
  777.             return '%s crashed with %s%s%s' % (
  778.                 os.path.basename(self['ExecutablePath']),
  779.                 signal_names.get(self.get('Signal'),
  780.                     'signal ' + self.get('Signal')),
  781.                 fn, arch_mismatch
  782.             )
  783.  
  784.         # Python exception
  785.         if self.has_key('Traceback') and \
  786.             self.has_key('ExecutablePath'):
  787.  
  788.             trace = self['Traceback'].splitlines()
  789.  
  790.             if len(trace) < 1:
  791.                 return None
  792.             if len(trace) < 3:
  793.                 return '%s crashed with %s' % (
  794.                     os.path.basename(self['ExecutablePath']),
  795.                     trace[0])
  796.  
  797.             trace_re = re.compile('^\s*File.* in (.+)$')
  798.             i = len(trace)-1
  799.             function = 'unknown'
  800.             while i >= 0:
  801.                 m = trace_re.match(trace[i])
  802.                 if m:
  803.                     function = m.group(1)
  804.                     break
  805.                 i -= 1
  806.  
  807.             return '%s crashed with %s in %s()' % (
  808.                 os.path.basename(self['ExecutablePath']),
  809.                 trace[-1].split(':')[0],
  810.                 function
  811.             )
  812.  
  813.         # package problem
  814.         if self.get('ProblemType') == 'Package' and \
  815.             self.has_key('Package'):
  816.  
  817.             title = 'package %s failed to install/upgrade' % \
  818.                 self['Package']
  819.             if self.get('ErrorMessage'):
  820.                 title += ': ' + self['ErrorMessage'].splitlines()[-1]
  821.  
  822.             return title
  823.  
  824.         if self.get('ProblemType') == 'KernelOops' and \
  825.             self.has_key('OopsText'):
  826.  
  827.             oops = self['OopsText']
  828.             if oops.startswith('------------[ cut here ]------------'):
  829.                 title = oops.split('\n', 2)[1]
  830.             else:
  831.                 title = oops.split('\n', 1)[0]
  832.  
  833.             return title
  834.  
  835.         if self.get('ProblemType') == 'KernelOops' and \
  836.             self.has_key('Failure'):
  837.             
  838.             # Title the report with suspend or hibernate as appropriate,
  839.             # and mention any non-free modules loaded up front.
  840.             title = ''
  841.             if 'MachineType' in self:
  842.                 title += '[' + self['MachineType'] + '] '
  843.             title += self['Failure'] + ' failure'
  844.             if 'NonfreeKernelModules' in self:
  845.                 title += ' [non-free: ' + self['NonfreeKernelModules'] + ']'
  846.             title += '\n'
  847.  
  848.             return title
  849.  
  850.  
  851.         return None
  852.  
  853.     def obsolete_packages(self):
  854.         '''Check Package: and Dependencies: for obsolete packages and return a
  855.         list of them.'''
  856.  
  857.         obsolete = []
  858.         for l in (self['Package'] + '\n' + self.get('Dependencies', '')).splitlines():
  859.             if not l:
  860.                 continue
  861.             pkg, ver = l.split()[:2]
  862.             avail = packaging.get_available_version(pkg)
  863.             if ver != None and ver != 'None' and avail != None and \
  864.                 packaging.compare_versions(ver, avail) < 0:
  865.                 obsolete.append(pkg)
  866.         return obsolete
  867.  
  868.     def crash_signature(self):
  869.         '''Calculate a signature string for a crash suitable for identifying
  870.         duplicates.
  871.  
  872.         For signal crashes this the concatenation of ExecutablePath, Signal
  873.         number, and StacktraceTop function names, separated by a colon. If
  874.         StacktraceTop has unknown functions or the report lacks any of those
  875.         fields, return None.
  876.         
  877.         For Python crashes, this concatenates the ExecutablePath, exception
  878.         name, and Traceback function names, again separated by a colon.'''
  879.  
  880.         if not self.has_key('ExecutablePath'):
  881.             return None
  882.  
  883.         # signal crashes
  884.         if self.has_key('StacktraceTop') and self.has_key('Signal'):
  885.             sig = '%s:%s' % (self['ExecutablePath'], self['Signal'])
  886.             bt_fn_re = re.compile('^(?:([\w:~]+).*|(<signal handler called>)\s*)$')
  887.  
  888.             for line in self['StacktraceTop'].splitlines():
  889.                 m = bt_fn_re.match(line)
  890.                 if m:
  891.                     sig += ':' + (m.group(1) or m.group(2))
  892.                 else:
  893.                     # this will also catch ??
  894.                     return None
  895.             return sig
  896.  
  897.         # Python crashes
  898.         if self.has_key('Traceback'):
  899.             trace = self['Traceback'].splitlines()
  900.  
  901.             sig = ''
  902.             if len(trace) == 1:
  903.                 # sometimes, Python exceptions do not have file references
  904.                 m = re.match('(\w+): ', trace[0])
  905.                 if m:
  906.                     return self['ExecutablePath'] + ':' + m.group(1)
  907.                 else:
  908.                     return None
  909.             elif len(trace) < 3:
  910.                 return None
  911.  
  912.             for l in trace:
  913.                 if l.startswith('  File'):
  914.                     sig += ':' + l.split()[-1]
  915.  
  916.             return self['ExecutablePath'] + ':' + trace[-1].split(':')[0] + sig
  917.  
  918.         return None
  919.  
  920.     def anonymize(self):
  921.         '''Remove user identifying strings from the report.
  922.  
  923.         This particularly removes the user name, host name, and IPs
  924.         from attributes which contain data read from the environment, and
  925.         removes the ProcCwd attribute completely.
  926.         '''
  927.         replacements = {}
  928.         if (os.getuid() > 0):
  929.             # do not replace "root"
  930.             p = pwd.getpwuid(os.getuid())
  931.             if len(p[0]) >= 2:
  932.                 replacements[p[0]] = 'username'
  933.             replacements[p[5]] = '/home/username'
  934.  
  935.             for s in p[4].split(','):
  936.                 s = s.strip()
  937.                 if len(s) > 2:
  938.                     replacements[s] = 'User Name'
  939.  
  940.         hostname = os.uname()[1]
  941.         if len(hostname) >= 2:
  942.             replacements[hostname] = 'hostname'
  943.  
  944.         try:
  945.             del self['ProcCwd']
  946.         except KeyError:
  947.             pass
  948.  
  949.         for k in self:
  950.             if k.startswith('Proc') or 'Stacktrace' in k or \
  951.                 k in ['Traceback', 'PythonArgs']:
  952.                 for old, new in replacements.iteritems():
  953.                     if hasattr(self[k], 'isspace'):
  954.                         self[k] = self[k].replace(old, new)
  955.  
  956. #
  957. # Unit test
  958. #
  959.  
  960. import unittest, shutil, signal, time
  961. from cStringIO import StringIO
  962.  
  963. class _ApportReportTest(unittest.TestCase):
  964.     def test_add_package_info(self):
  965.         '''add_package_info().'''
  966.  
  967.         # determine bash version
  968.         bashversion = packaging.get_version('bash')
  969.  
  970.         pr = Report()
  971.         self.assertRaises(ValueError, pr.add_package_info, 'nonexistant_package')
  972.  
  973.         pr.add_package_info('bash')
  974.         self.assertEqual(pr['Package'], 'bash ' + bashversion.strip())
  975.         self.assertEqual(pr['SourcePackage'], 'bash')
  976.         self.assert_('libc' in pr['Dependencies'])
  977.  
  978.         # test without specifying a package, but with ExecutablePath
  979.         pr = Report()
  980.         self.assertRaises(KeyError, pr.add_package_info)
  981.         pr['ExecutablePath'] = '/bin/bash'
  982.         pr.add_package_info()
  983.         self.assertEqual(pr['Package'], 'bash ' + bashversion.strip())
  984.         self.assertEqual(pr['SourcePackage'], 'bash')
  985.         self.assert_('libc' in pr['Dependencies'])
  986.         # check for stray empty lines
  987.         self.assert_('\n\n' not in pr['Dependencies'])
  988.         self.assert_(pr.has_key('PackageArchitecture'))
  989.  
  990.         pr = Report()
  991.         pr['ExecutablePath'] = '/nonexisting'
  992.         pr.add_package_info()
  993.         self.assert_(not pr.has_key('Package'))
  994.  
  995.     def test_add_os_info(self):
  996.         '''add_os_info().'''
  997.  
  998.         pr = Report()
  999.         pr.add_os_info()
  1000.         self.assert_(pr['Uname'].startswith('Linux'))
  1001.         self.assert_(type(pr['DistroRelease']) == type(''))
  1002.         self.assert_(pr['Architecture'])
  1003.  
  1004.     def test_add_user_info(self):
  1005.         '''add_user_info().'''
  1006.  
  1007.         pr = Report()
  1008.         pr.add_user_info()
  1009.         self.assert_(pr.has_key('UserGroups'))
  1010.  
  1011.         # double-check that user group names are removed
  1012.         for g in pr['UserGroups'].split():
  1013.             self.assert_(grp.getgrnam(g).gr_gid < 1000)
  1014.         self.assert_(grp.getgrgid(os.getgid()).gr_name not in pr['UserGroups'])
  1015.  
  1016.     def test_add_proc_info(self):
  1017.         '''add_proc_info().'''
  1018.  
  1019.         # set test environment
  1020.         assert os.environ.has_key('LANG'), 'please set $LANG for this test'
  1021.         assert os.environ.has_key('USER'), 'please set $USER for this test'
  1022.         assert os.environ.has_key('PWD'), '$PWD is not set'
  1023.  
  1024.         # check without additional safe environment variables
  1025.         pr = Report()
  1026.         pr.add_proc_info()
  1027.         self.assert_(set(['ProcEnviron', 'ProcMaps', 'ProcCmdline',
  1028.             'ProcMaps']).issubset(set(pr.keys())), 'report has required fields')
  1029.         self.assert_('LANG='+os.environ['LANG'] in pr['ProcEnviron'])
  1030.         self.assert_('USER' not in pr['ProcEnviron'])
  1031.         self.assert_('PWD' not in pr['ProcEnviron'])
  1032.  
  1033.         # check with one additional safe environment variable
  1034.         pr = Report()
  1035.         pr.add_proc_info(extraenv=['PWD'])
  1036.         self.assert_('USER' not in pr['ProcEnviron'])
  1037.         self.assert_('PWD='+os.environ['PWD'] in pr['ProcEnviron'])
  1038.  
  1039.         # check process from other user
  1040.         assert os.getuid() != 0, 'please do not run this test as root for this check.'
  1041.         pr = Report()
  1042.         self.assertRaises(OSError, pr.add_proc_info, 1) # EPERM for init process
  1043.         self.assert_('init' in pr['ProcStatus'], pr['ProcStatus'])
  1044.         self.assert_(pr['ProcEnviron'].startswith('Error:'), pr['ProcEnviron'])
  1045.         self.assert_(not pr.has_key('InterpreterPath'))
  1046.  
  1047.         # check escaping of ProcCmdline
  1048.         p = subprocess.Popen(['cat', '/foo bar', '\\h', '\\ \\', '-'],
  1049.             stdin=subprocess.PIPE, stdout=subprocess.PIPE,
  1050.             stderr=subprocess.PIPE, close_fds=True)
  1051.         assert p.pid
  1052.         # wait until /proc/pid/cmdline exists
  1053.         while not open('/proc/%i/cmdline' % p.pid).read():
  1054.             time.sleep(0.1)
  1055.         pr = Report()
  1056.         pr.add_proc_info(pid=p.pid)
  1057.         p.communicate('\n')
  1058.         self.assertEqual(pr['ProcCmdline'], 'cat /foo\ bar \\\\h \\\\\\ \\\\ -')
  1059.         self.assertEqual(pr['ExecutablePath'], '/bin/cat')
  1060.         self.assert_(not pr.has_key('InterpreterPath'))
  1061.         self.assertTrue('/bin/cat' in pr['ProcMaps'])
  1062.         self.assertTrue('[stack]' in pr['ProcMaps'])
  1063.  
  1064.         # check correct handling of executable symlinks
  1065.         assert os.path.islink('/bin/sh'), '/bin/sh needs to be a symlink for this test'
  1066.         p = subprocess.Popen(['sh'], stdin=subprocess.PIPE,
  1067.             close_fds=True)
  1068.         assert p.pid
  1069.         # wait until /proc/pid/cmdline exists
  1070.         while not open('/proc/%i/cmdline' % p.pid).read():
  1071.             time.sleep(0.1)
  1072.         pr = Report()
  1073.         pr.add_proc_info(pid=p.pid)
  1074.         p.communicate('exit\n')
  1075.         self.failIf(pr.has_key('InterpreterPath'), pr.get('InterpreterPath'))
  1076.         self.assertEqual(pr['ExecutablePath'], os.path.realpath('/bin/sh'))
  1077.  
  1078.         # check correct handling of interpreted executables: shell
  1079.         p = subprocess.Popen(['zgrep', 'foo'], stdin=subprocess.PIPE,
  1080.             close_fds=True)
  1081.         assert p.pid
  1082.         # wait until /proc/pid/cmdline exists
  1083.         while not open('/proc/%i/cmdline' % p.pid).read():
  1084.             time.sleep(0.1)
  1085.         pr = Report()
  1086.         pr.add_proc_info(pid=p.pid)
  1087.         p.communicate('\n')
  1088.         self.assert_(pr['ExecutablePath'].endswith('bin/zgrep'))
  1089.         self.assertEqual(pr['InterpreterPath'],
  1090.             os.path.realpath(open(pr['ExecutablePath']).readline().strip()[2:]))
  1091.         self.assertTrue('[stack]' in pr['ProcMaps'])
  1092.  
  1093.         # check correct handling of interpreted executables: python
  1094.         (fd, testscript) = tempfile.mkstemp()
  1095.         os.write(fd, '''#!/usr/bin/python
  1096. import sys
  1097. sys.stdin.readline()
  1098. ''')
  1099.         os.close(fd)
  1100.         os.chmod(testscript, 0755)
  1101.         p = subprocess.Popen([testscript], stdin=subprocess.PIPE,
  1102.             stderr=subprocess.PIPE, close_fds=True)
  1103.         assert p.pid
  1104.         # wait until /proc/pid/cmdline exists
  1105.         while not open('/proc/%i/cmdline' % p.pid).read():
  1106.             time.sleep(0.1)
  1107.         pr = Report()
  1108.         pr.add_proc_info(pid=p.pid)
  1109.         p.communicate('\n')
  1110.         os.unlink(testscript)
  1111.         self.assertEqual(pr['ExecutablePath'], testscript)
  1112.         self.assert_('python' in pr['InterpreterPath'])
  1113.         self.assertTrue('python' in pr['ProcMaps'])
  1114.         self.assertTrue('[stack]' in pr['ProcMaps'])
  1115.  
  1116.         # test process is gone, should complain about nonexisting PID
  1117.         self.assertRaises(OSError, pr.add_proc_info, p.pid)
  1118.  
  1119.     def test_add_path_classification(self):
  1120.         '''classification of $PATH.'''
  1121.  
  1122.         # system default
  1123.         p = subprocess.Popen(['cat'], stdin=subprocess.PIPE, 
  1124.             env={'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games'})
  1125.         time.sleep(0.1)
  1126.         r = Report()
  1127.         r.add_proc_environ(pid=p.pid)
  1128.         p.communicate('')
  1129.         self.failIf('PATH' in r['ProcEnviron'], 
  1130.             'system default $PATH should be filtered out')
  1131.  
  1132.         # no user paths
  1133.         p = subprocess.Popen(['cat'], stdin=subprocess.PIPE, 
  1134.             env={'PATH': '/usr/sbin:/usr/bin:/sbin:/bin'})
  1135.         time.sleep(0.1)
  1136.         r = Report()
  1137.         r.add_proc_environ(pid=p.pid)
  1138.         p.communicate('')
  1139.         self.assert_('PATH=(custom, no user)' in r['ProcEnviron'], 
  1140.             'PATH is customized without user paths')
  1141.  
  1142.         # user paths
  1143.         p = subprocess.Popen(['cat'], stdin=subprocess.PIPE, 
  1144.             env={'PATH': '/home/pitti:/usr/sbin:/usr/bin:/sbin:/bin'})
  1145.         time.sleep(0.1)
  1146.         r = Report()
  1147.         r.add_proc_environ(pid=p.pid)
  1148.         p.communicate('')
  1149.         self.assert_('PATH=(custom, user)' in r['ProcEnviron'], 
  1150.             'PATH is customized with user paths')
  1151.  
  1152.     def test_check_interpreted(self):
  1153.         '''_check_interpreted().'''
  1154.  
  1155.         # standard ELF binary
  1156.         f = tempfile.NamedTemporaryFile()
  1157.         pr = Report()
  1158.         pr['ExecutablePath'] = '/usr/bin/gedit'
  1159.         pr['ProcStatus'] = 'Name:\tgedit'
  1160.         pr['ProcCmdline'] = 'gedit\0/' + f.name
  1161.         pr._check_interpreted()
  1162.         self.assertEqual(pr['ExecutablePath'], '/usr/bin/gedit')
  1163.         self.failIf(pr.has_key('InterpreterPath'))
  1164.         f.close()
  1165.  
  1166.         # bogus argv[0]
  1167.         pr = Report()
  1168.         pr['ExecutablePath'] = '/bin/dash'
  1169.         pr['ProcStatus'] = 'Name:\tznonexisting'
  1170.         pr['ProcCmdline'] = 'nonexisting\0/foo'
  1171.         pr._check_interpreted()
  1172.         self.assertEqual(pr['ExecutablePath'], '/bin/dash')
  1173.         self.failIf(pr.has_key('InterpreterPath'))
  1174.  
  1175.         # standard sh script
  1176.         pr = Report()
  1177.         pr['ExecutablePath'] = '/bin/dash'
  1178.         pr['ProcStatus'] = 'Name:\tzgrep'
  1179.         pr['ProcCmdline'] = '/bin/sh\0/bin/zgrep\0foo'
  1180.         pr._check_interpreted()
  1181.         self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
  1182.         self.assertEqual(pr['InterpreterPath'], '/bin/dash')
  1183.  
  1184.         # standard sh script when being called explicitly with interpreter
  1185.         pr = Report()
  1186.         pr['ExecutablePath'] = '/bin/dash'
  1187.         pr['ProcStatus'] = 'Name:\tdash'
  1188.         pr['ProcCmdline'] = '/bin/sh\0/bin/zgrep\0foo'
  1189.         pr._check_interpreted()
  1190.         self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
  1191.         self.assertEqual(pr['InterpreterPath'], '/bin/dash')
  1192.  
  1193.         # special case mono scheme: beagled-helper (use zgrep to make the test
  1194.         # suite work if mono or beagle are not installed)
  1195.         pr = Report()
  1196.         pr['ExecutablePath'] = '/usr/bin/mono'
  1197.         pr['ProcStatus'] = 'Name:\tzgrep'
  1198.         pr['ProcCmdline'] = 'zgrep\0--debug\0/bin/zgrep'
  1199.         pr._check_interpreted()
  1200.         self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
  1201.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/mono')
  1202.  
  1203.         # special case mono scheme: banshee (use zgrep to make the test
  1204.         # suite work if mono or beagle are not installed)
  1205.         pr = Report()
  1206.         pr['ExecutablePath'] = '/usr/bin/mono'
  1207.         pr['ProcStatus'] = 'Name:\tzgrep'
  1208.         pr['ProcCmdline'] = 'zgrep\0/bin/zgrep'
  1209.         pr._check_interpreted()
  1210.         self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
  1211.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/mono')
  1212.  
  1213.         # fail on files we shouldn't have access to when name!=argv[0]
  1214.         pr = Report()
  1215.         pr['ExecutablePath'] = '/usr/bin/python'
  1216.         pr['ProcStatus'] = 'Name:\tznonexisting'
  1217.         pr['ProcCmdline'] = 'python\0/etc/shadow'
  1218.         pr._check_interpreted()
  1219.         self.assertEqual(pr['ExecutablePath'], '/usr/bin/python')
  1220.         self.failIf(pr.has_key('InterpreterPath'))
  1221.  
  1222.         # succeed on files we should have access to when name!=argv[0]
  1223.         pr = Report()
  1224.         pr['ExecutablePath'] = '/usr/bin/python'
  1225.         pr['ProcStatus'] = 'Name:\tznonexisting'
  1226.         pr['ProcCmdline'] = 'python\0/etc/passwd'
  1227.         pr._check_interpreted()
  1228.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
  1229.         self.assertEqual(pr['ExecutablePath'], '/etc/passwd')
  1230.  
  1231.         # fail on files we shouldn't have access to when name==argv[0]
  1232.         pr = Report()
  1233.         pr['ExecutablePath'] = '/usr/bin/python'
  1234.         pr['ProcStatus'] = 'Name:\tshadow'
  1235.         pr['ProcCmdline'] = '../etc/shadow'
  1236.         pr._check_interpreted()
  1237.         self.assertEqual(pr['ExecutablePath'], '/usr/bin/python')
  1238.         self.failIf(pr.has_key('InterpreterPath'))
  1239.  
  1240.         # succeed on files we should have access to when name==argv[0]
  1241.         pr = Report()
  1242.         pr['ExecutablePath'] = '/usr/bin/python'
  1243.         pr['ProcStatus'] = 'Name:\tpasswd'
  1244.         pr['ProcCmdline'] = '../etc/passwd'
  1245.         pr._check_interpreted()
  1246.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
  1247.         self.assertEqual(pr['ExecutablePath'], '/bin/../etc/passwd')
  1248.  
  1249.         # interactive python process
  1250.         pr = Report()
  1251.         pr['ExecutablePath'] = '/usr/bin/python'
  1252.         pr['ProcStatus'] = 'Name:\tpython'
  1253.         pr['ProcCmdline'] = 'python'
  1254.         pr._check_interpreted()
  1255.         self.assertEqual(pr['ExecutablePath'], '/usr/bin/python')
  1256.         self.failIf(pr.has_key('InterpreterPath'))
  1257.  
  1258.         # python script (abuse /bin/bash since it must exist)
  1259.         pr = Report()
  1260.         pr['ExecutablePath'] = '/usr/bin/python'
  1261.         pr['ProcStatus'] = 'Name:\tbash'
  1262.         pr['ProcCmdline'] = 'python\0/bin/bash'
  1263.         pr._check_interpreted()
  1264.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
  1265.         self.assertEqual(pr['ExecutablePath'], '/bin/bash')
  1266.  
  1267.         # python script with options (abuse /bin/bash since it must exist)
  1268.         pr = Report()
  1269.         pr['ExecutablePath'] = '/usr/bin/python'
  1270.         pr['ProcStatus'] = 'Name:\tbash'
  1271.         pr['ProcCmdline'] = 'python\0-OO\0/bin/bash'
  1272.         pr._check_interpreted()
  1273.         self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
  1274.         self.assertEqual(pr['ExecutablePath'], '/bin/bash')
  1275.  
  1276.     @classmethod
  1277.     def _generate_sigsegv_report(klass, file=None):
  1278.         '''Create a test executable which will die with a SIGSEGV, generate a
  1279.         core dump for it, create a problem report with those two arguments
  1280.         (ExecutablePath and CoreDump) and call add_gdb_info().
  1281.  
  1282.         If file is given, the report is written into it. Return the Report.'''
  1283.  
  1284.         workdir = None
  1285.         orig_cwd = os.getcwd()
  1286.         pr = Report()
  1287.         try:
  1288.             workdir = tempfile.mkdtemp()
  1289.             atexit.register(shutil.rmtree, workdir)
  1290.             os.chdir(workdir)
  1291.  
  1292.             # create a test executable
  1293.             open('crash.c', 'w').write('''
  1294. int f(x) {
  1295.     int* p = 0; *p = x;
  1296.     return x+1;
  1297. }
  1298. int main() { return f(42); }
  1299. ''')
  1300.             assert subprocess.call(['gcc', '-g', 'crash.c', '-o', 'crash']) == 0
  1301.             assert os.path.exists('crash')
  1302.  
  1303.             # call it through gdb and dump core
  1304.             subprocess.call(['gdb', '--batch', '--ex', 'run', '--ex',
  1305.                 'generate-core-file core', './crash'], stdout=subprocess.PIPE)
  1306.             assert os.path.exists('core')
  1307.             assert subprocess.call(['readelf', '-n', 'core'],
  1308.                 stdout=subprocess.PIPE) == 0
  1309.  
  1310.             pr['ExecutablePath'] = os.path.join(workdir, 'crash')
  1311.             pr['CoreDump'] = (os.path.join(workdir, 'core'),)
  1312.             pr['Signal'] = '11'
  1313.  
  1314.             pr.add_gdb_info()
  1315.             if file:
  1316.                 pr.write(file)
  1317.                 file.flush()
  1318.         finally:
  1319.             os.chdir(orig_cwd)
  1320.  
  1321.         return pr
  1322.  
  1323.     def _validate_gdb_fields(self,pr):
  1324.         self.assert_(pr.has_key('Stacktrace'))
  1325.         self.assert_(pr.has_key('ThreadStacktrace'))
  1326.         self.assert_(pr.has_key('StacktraceTop'))
  1327.         self.assert_(pr.has_key('Registers'))
  1328.         self.assert_(pr.has_key('Disassembly'))
  1329.         self.assert_('(no debugging symbols found)' not in pr['Stacktrace'])
  1330.         self.assert_('Core was generated by' not in pr['Stacktrace'], pr['Stacktrace'])
  1331.         self.assert_(not re.match(r"(?s)(^|.*\n)#0  [^\n]+\n#0  ",
  1332.                                   pr['Stacktrace']))
  1333.         self.assert_('#0  0x' in pr['Stacktrace'])
  1334.         self.assert_('#1  0x' in pr['Stacktrace'])
  1335.         self.assert_('#0  0x' in pr['ThreadStacktrace'])
  1336.         self.assert_('#1  0x' in pr['ThreadStacktrace'])
  1337.         self.assert_('Thread 1 (process' in pr['ThreadStacktrace'])
  1338.         self.assert_(len(pr['StacktraceTop'].splitlines()) <= 5)
  1339.  
  1340.     def test_add_gdb_info(self):
  1341.         '''add_gdb_info() with core dump file reference.'''
  1342.  
  1343.         pr = Report()
  1344.         # should not throw an exception for missing fields
  1345.         pr.add_gdb_info()
  1346.  
  1347.         pr = self._generate_sigsegv_report()
  1348.         self._validate_gdb_fields(pr)
  1349.         self.assertEqual(pr['StacktraceTop'], 'f (x=42) at crash.c:3\nmain () at crash.c:6')
  1350.  
  1351.     def test_add_gdb_info_load(self):
  1352.         '''add_gdb_info() with inline core dump.'''
  1353.  
  1354.         rep = tempfile.NamedTemporaryFile()
  1355.         self._generate_sigsegv_report(rep)
  1356.         rep.seek(0)
  1357.  
  1358.         pr = Report()
  1359.         pr.load(open(rep.name))
  1360.         pr.add_gdb_info()
  1361.  
  1362.         self._validate_gdb_fields(pr)
  1363.  
  1364.     def test_add_gdb_info_script(self):
  1365.         '''add_gdb_info() with a script.'''
  1366.  
  1367.         (fd, coredump) = tempfile.mkstemp()
  1368.         (fd2, script) = tempfile.mkstemp()
  1369.         try:
  1370.             os.close(fd)
  1371.             os.close(fd2)
  1372.  
  1373.             # create a test script which produces a core dump for us
  1374.             open(script, 'w').write('''#!/bin/bash
  1375. gdb --batch --ex 'generate-core-file %s' --pid $$ >/dev/null''' % coredump)
  1376.             os.chmod(script, 0755)
  1377.  
  1378.             # call script and verify that it gives us a proper ELF core dump
  1379.             assert subprocess.call([script]) == 0
  1380.             assert subprocess.call(['readelf', '-n', coredump],
  1381.                 stdout=subprocess.PIPE) == 0
  1382.  
  1383.             pr = Report()
  1384.             pr['InterpreterPath'] = '/bin/bash'
  1385.             pr['ExecutablePath'] = script
  1386.             pr['CoreDump'] = (coredump,)
  1387.             pr.add_gdb_info()
  1388.         finally:
  1389.             os.unlink(coredump)
  1390.             os.unlink(script)
  1391.  
  1392.         self._validate_gdb_fields(pr)
  1393.         self.assert_('libc.so' in pr['Stacktrace'] or 'in execute_command' in pr['Stacktrace'])
  1394.  
  1395.     def test_search_bug_patterns(self):
  1396.         '''search_bug_patterns().'''
  1397.  
  1398.         pdir = None
  1399.         try:
  1400.             pdir = tempfile.mkdtemp()
  1401.  
  1402.             # create some test patterns
  1403.             open(os.path.join(pdir, 'bash.xml'), 'w').write('''<?xml version="1.0"?>
  1404. <patterns>
  1405.     <pattern url="http://bugtracker.net/bugs/1">
  1406.         <re key="Foo">ba.*r</re>
  1407.     </pattern>
  1408.     <pattern url="http://bugtracker.net/bugs/2">
  1409.         <re key="Foo">write_(hello|goodbye)</re>
  1410.         <re key="Package">^\S* 1-2$</re>
  1411.     </pattern>
  1412. </patterns>''')
  1413.  
  1414.             open(os.path.join(pdir, 'coreutils.xml'), 'w').write('''<?xml version="1.0"?>
  1415. <patterns>
  1416.     <pattern url="http://bugtracker.net/bugs/3">
  1417.         <re key="Bar">^1$</re>
  1418.     </pattern>
  1419.     <pattern url="http://bugtracker.net/bugs/4">
  1420.         <re key="Bar">*</re> <!-- invalid RE -->
  1421.     </pattern>
  1422. </patterns>''')
  1423.  
  1424.             # invalid XML
  1425.             open(os.path.join(pdir, 'invalid.xml'), 'w').write('''<?xml version="1.0"?>
  1426. </patterns>''')
  1427.  
  1428.             # create some reports
  1429.             r_bash = Report()
  1430.             r_bash['Package'] = 'bash 1-2'
  1431.             r_bash['Foo'] = 'bazaar'
  1432.  
  1433.             r_coreutils = Report()
  1434.             r_coreutils['Package'] = 'coreutils 1'
  1435.             r_coreutils['Bar'] = '1'
  1436.  
  1437.             r_invalid = Report()
  1438.             r_invalid['Package'] = 'invalid 1'
  1439.  
  1440.             # positive match cases
  1441.             self.assertEqual(r_bash.search_bug_patterns(pdir), 'http://bugtracker.net/bugs/1')
  1442.             r_bash['Foo'] = 'write_goodbye'
  1443.             self.assertEqual(r_bash.search_bug_patterns(pdir), 'http://bugtracker.net/bugs/2')
  1444.             self.assertEqual(r_coreutils.search_bug_patterns(pdir), 'http://bugtracker.net/bugs/3')
  1445.  
  1446.             # match on source package
  1447.             r_bash['Package'] = 'bash-static 1-2'
  1448.             self.assertEqual(r_bash.search_bug_patterns(pdir), None)
  1449.             r_bash['SourcePackage'] = 'bash'
  1450.             self.assertEqual(r_bash.search_bug_patterns(pdir), 'http://bugtracker.net/bugs/2')
  1451.  
  1452.             # negative match cases
  1453.             r_bash['Package'] = 'bash 1-21'
  1454.             self.assertEqual(r_bash.search_bug_patterns(pdir), None,
  1455.                 'does not match on wrong bash version')
  1456.             r_bash['Foo'] = 'zz'
  1457.             self.assertEqual(r_bash.search_bug_patterns(pdir), None,
  1458.                 'does not match on wrong Foo value')
  1459.             r_coreutils['Bar'] = '11'
  1460.             self.assertEqual(r_coreutils.search_bug_patterns(pdir), None,
  1461.                 'does not match on wrong Bar value')
  1462.  
  1463.             # various errors to check for robustness (no exceptions, just None
  1464.             # return value)
  1465.             del r_coreutils['Bar']
  1466.             self.assertEqual(r_coreutils.search_bug_patterns(pdir), None,
  1467.                 'does not match on nonexisting key')
  1468.             self.assertEqual(r_invalid.search_bug_patterns(pdir), None,
  1469.                 'gracefully handles invalid XML')
  1470.             r_coreutils['Package'] = 'other 2'
  1471.             self.assertEqual(r_coreutils.search_bug_patterns(pdir), None,
  1472.                 'gracefully handles nonexisting package XML file')
  1473.             self.assertEqual(r_bash.search_bug_patterns('file:///nonexisting/directory/'), None,
  1474.                 'gracefully handles nonexisting base path')
  1475.             # existing host, but no bug patterns
  1476.             self.assertEqual(r_bash.search_bug_patterns('http://security.ubuntu.com/'), None,
  1477.                 'gracefully handles base path without bug patterns')
  1478.             # nonexisting host
  1479.             self.assertEqual(r_bash.search_bug_patterns('http://nonexisting.domain/'), None,
  1480.                 'gracefully handles nonexisting URL domain')
  1481.         finally:
  1482.             if pdir:
  1483.                 shutil.rmtree(pdir)
  1484.  
  1485.     def test_add_hooks_info(self):
  1486.         '''add_hooks_info().'''
  1487.  
  1488.         global _hook_dir
  1489.         global _common_hook_dir
  1490.         orig_hook_dir = _hook_dir
  1491.         _hook_dir = tempfile.mkdtemp()
  1492.         orig_common_hook_dir = _common_hook_dir
  1493.         _common_hook_dir = tempfile.mkdtemp()
  1494.         try:
  1495.             open(os.path.join(_hook_dir, 'foo.py'), 'w').write('''
  1496. def add_info(report):
  1497.     report['Field1'] = 'Field 1'
  1498.     report['Field2'] = 'Field 2\\nBla'
  1499. ''')
  1500.  
  1501.             open(os.path.join(_common_hook_dir, 'foo1.py'), 'w').write('''
  1502. def add_info(report):
  1503.     report['CommonField1'] = 'CommonField 1'
  1504. ''')
  1505.             open(os.path.join(_common_hook_dir, 'foo2.py'), 'w').write('''
  1506. def add_info(report):
  1507.     report['CommonField2'] = 'CommonField 2'
  1508. ''')
  1509.  
  1510.             # should only catch .py files
  1511.             open(os.path.join(_common_hook_dir, 'notme'), 'w').write('''
  1512. def add_info(report):
  1513.     report['BadField'] = 'XXX'
  1514. ''')
  1515.             r = Report()
  1516.             r['Package'] = 'bar'
  1517.             # should not throw any exceptions
  1518.             r.add_hooks_info()
  1519.             self.assertEqual(set(r.keys()), set(['ProblemType', 'Date',
  1520.                 'Package', 'CommonField1', 'CommonField2']), 
  1521.                 'report has required fields')
  1522.  
  1523.             r = Report()
  1524.             r['Package'] = 'baz 1.2-3'
  1525.             # should not throw any exceptions
  1526.             r.add_hooks_info()
  1527.             self.assertEqual(set(r.keys()), set(['ProblemType', 'Date',
  1528.                 'Package', 'CommonField1', 'CommonField2']), 
  1529.                 'report has required fields')
  1530.  
  1531.             r = Report()
  1532.             r['Package'] = 'foo'
  1533.             r.add_hooks_info()
  1534.             self.assertEqual(set(r.keys()), set(['ProblemType', 'Date',
  1535.                 'Package', 'Field1', 'Field2', 'CommonField1',
  1536.                 'CommonField2']), 'report has required fields')
  1537.             self.assertEqual(r['Field1'], 'Field 1')
  1538.             self.assertEqual(r['Field2'], 'Field 2\nBla')
  1539.             self.assertEqual(r['CommonField1'], 'CommonField 1')
  1540.             self.assertEqual(r['CommonField2'], 'CommonField 2')
  1541.  
  1542.             r = Report()
  1543.             r['Package'] = 'foo 4.5-6'
  1544.             r.add_hooks_info()
  1545.             self.assertEqual(set(r.keys()), set(['ProblemType', 'Date',
  1546.                 'Package', 'Field1', 'Field2', 'CommonField1',
  1547.                 'CommonField2']), 'report has required fields')
  1548.             self.assertEqual(r['Field1'], 'Field 1')
  1549.             self.assertEqual(r['Field2'], 'Field 2\nBla')
  1550.             self.assertEqual(r['CommonField1'], 'CommonField 1')
  1551.             self.assertEqual(r['CommonField2'], 'CommonField 2')
  1552.  
  1553.             # source package hook
  1554.             open(os.path.join(_hook_dir, 'source_foo.py'), 'w').write('''
  1555. def add_info(report):
  1556.     report['Field1'] = 'Field 1'
  1557.     report['Field2'] = 'Field 2\\nBla'
  1558. ''')
  1559.             r = Report()
  1560.             r['SourcePackage'] = 'foo'
  1561.             r['Package'] = 'libfoo 3'
  1562.             r.add_hooks_info()
  1563.             self.assertEqual(set(r.keys()), set(['ProblemType', 'Date',
  1564.                 'Package', 'SourcePackage', 'Field1', 'Field2', 'CommonField1',
  1565.                 'CommonField2']), 'report has required fields')
  1566.             self.assertEqual(r['Field1'], 'Field 1')
  1567.             self.assertEqual(r['Field2'], 'Field 2\nBla')
  1568.             self.assertEqual(r['CommonField1'], 'CommonField 1')
  1569.             self.assertEqual(r['CommonField2'], 'CommonField 2')
  1570.  
  1571.         finally:
  1572.             shutil.rmtree(_hook_dir)
  1573.             shutil.rmtree(_common_hook_dir)
  1574.             _hook_dir = orig_hook_dir
  1575.             _common_hook_dir = orig_common_hook_dir
  1576.  
  1577.     def test_ignoring(self):
  1578.         '''mark_ignore() and check_ignored().'''
  1579.  
  1580.         global _ignore_file
  1581.         orig_ignore_file = _ignore_file
  1582.         workdir = tempfile.mkdtemp()
  1583.         _ignore_file = os.path.join(workdir, 'ignore.xml')
  1584.         try:
  1585.             open(os.path.join(workdir, 'bash'), 'w').write('bash')
  1586.             open(os.path.join(workdir, 'crap'), 'w').write('crap')
  1587.  
  1588.             bash_rep = Report()
  1589.             bash_rep['ExecutablePath'] = os.path.join(workdir, 'bash')
  1590.             crap_rep = Report()
  1591.             crap_rep['ExecutablePath'] = os.path.join(workdir, 'crap')
  1592.             # must be able to deal with executables that do not exist any more
  1593.             cp_rep = Report()
  1594.             cp_rep['ExecutablePath'] = os.path.join(workdir, 'cp')
  1595.  
  1596.             # no ignores initially
  1597.             self.assertEqual(bash_rep.check_ignored(), False)
  1598.             self.assertEqual(crap_rep.check_ignored(), False)
  1599.             self.assertEqual(cp_rep.check_ignored(), False)
  1600.  
  1601.             # ignore crap now
  1602.             crap_rep.mark_ignore()
  1603.             self.assertEqual(bash_rep.check_ignored(), False)
  1604.             self.assertEqual(crap_rep.check_ignored(), True)
  1605.             self.assertEqual(cp_rep.check_ignored(), False)
  1606.  
  1607.             # ignore bash now
  1608.             bash_rep.mark_ignore()
  1609.             self.assertEqual(bash_rep.check_ignored(), True)
  1610.             self.assertEqual(crap_rep.check_ignored(), True)
  1611.             self.assertEqual(cp_rep.check_ignored(), False)
  1612.  
  1613.             # poke crap so that it has a newer timestamp
  1614.             time.sleep(1)
  1615.             open(os.path.join(workdir, 'crap'), 'w').write('crapnew')
  1616.             self.assertEqual(bash_rep.check_ignored(), True)
  1617.             self.assertEqual(crap_rep.check_ignored(), False)
  1618.             self.assertEqual(cp_rep.check_ignored(), False)
  1619.  
  1620.             # do not complain about an empty ignore file
  1621.             open(_ignore_file, 'w').write('')
  1622.             self.assertEqual(bash_rep.check_ignored(), False)
  1623.             self.assertEqual(crap_rep.check_ignored(), False)
  1624.             self.assertEqual(cp_rep.check_ignored(), False)
  1625.         finally:
  1626.             shutil.rmtree(workdir)
  1627.             _ignore_file = orig_ignore_file
  1628.  
  1629.     def test_blacklisting(self):
  1630.         '''check_ignored() for system-wise blacklist.'''
  1631.  
  1632.         global _blacklist_dir
  1633.         global _ignore_file
  1634.         orig_blacklist_dir = _blacklist_dir
  1635.         _blacklist_dir = tempfile.mkdtemp()
  1636.         orig_ignore_file = _ignore_file
  1637.         _ignore_file = '/nonexistant'
  1638.         try:
  1639.             bash_rep = Report()
  1640.             bash_rep['ExecutablePath'] = '/bin/bash'
  1641.             crap_rep = Report()
  1642.             crap_rep['ExecutablePath'] = '/bin/crap'
  1643.  
  1644.             # no ignores initially
  1645.             self.assertEqual(bash_rep.check_ignored(), False)
  1646.             self.assertEqual(crap_rep.check_ignored(), False)
  1647.  
  1648.             # should not stumble over comments
  1649.             open(os.path.join(_blacklist_dir, 'README'), 'w').write(
  1650.                 '# Ignore file\n#/bin/bash\n')
  1651.  
  1652.             # no ignores on nonmatching paths
  1653.             open(os.path.join(_blacklist_dir, 'bl1'), 'w').write(
  1654.                 '/bin/bas\n/bin/bashh\nbash\nbin/bash\n')
  1655.             self.assertEqual(bash_rep.check_ignored(), False)
  1656.             self.assertEqual(crap_rep.check_ignored(), False)
  1657.  
  1658.             # ignore crap now
  1659.             open(os.path.join(_blacklist_dir, 'bl_2'), 'w').write(
  1660.                 '/bin/crap\n')
  1661.             self.assertEqual(bash_rep.check_ignored(), False)
  1662.             self.assertEqual(crap_rep.check_ignored(), True)
  1663.  
  1664.             # ignore bash now
  1665.             open(os.path.join(_blacklist_dir, 'bl1'), 'a').write(
  1666.                 '/bin/bash\n')
  1667.             self.assertEqual(bash_rep.check_ignored(), True)
  1668.             self.assertEqual(crap_rep.check_ignored(), True)
  1669.         finally:
  1670.             shutil.rmtree(_blacklist_dir)
  1671.             _blacklist_dir = orig_blacklist_dir
  1672.             _ignore_file = orig_ignore_file
  1673.  
  1674.     def test_has_useful_stacktrace(self):
  1675.         '''has_useful_stacktrace().'''
  1676.  
  1677.         r = Report()
  1678.         self.failIf(r.has_useful_stacktrace())
  1679.  
  1680.         r['StacktraceTop'] = ''
  1681.         self.failIf(r.has_useful_stacktrace())
  1682.  
  1683.         r['StacktraceTop'] = '?? ()'
  1684.         self.failIf(r.has_useful_stacktrace())
  1685.  
  1686.         r['StacktraceTop'] = '?? ()\n?? ()'
  1687.         self.failIf(r.has_useful_stacktrace())
  1688.  
  1689.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\n?? ()'
  1690.         self.failIf(r.has_useful_stacktrace())
  1691.  
  1692.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\n?? ()\n?? ()\n?? ()'
  1693.         self.failIf(r.has_useful_stacktrace())
  1694.  
  1695.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so'
  1696.         self.assert_(r.has_useful_stacktrace())
  1697.  
  1698.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so\n?? ()'
  1699.         self.assert_(r.has_useful_stacktrace())
  1700.  
  1701.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so\n?? ()\n?? ()'
  1702.         self.assert_(r.has_useful_stacktrace())
  1703.  
  1704.         r['StacktraceTop'] = 'read () from /lib/libc.6.so\n?? ()\nfoo (i=1) from /usr/lib/libfoo.so\n?? ()\n?? ()'
  1705.         self.failIf(r.has_useful_stacktrace())
  1706.  
  1707.     def test_standard_title(self):
  1708.         '''standard_title().'''
  1709.  
  1710.         report = Report()
  1711.         self.assertEqual(report.standard_title(), None)
  1712.  
  1713.         # named signal crash
  1714.         report['Signal'] = '11'
  1715.         report['ExecutablePath'] = '/bin/bash'
  1716.         report['StacktraceTop'] = '''foo()
  1717. bar(x=3)
  1718. baz()
  1719. '''
  1720.         self.assertEqual(report.standard_title(),
  1721.             'bash crashed with SIGSEGV in foo()')
  1722.  
  1723.         # unnamed signal crash
  1724.         report['Signal'] = '42'
  1725.         self.assertEqual(report.standard_title(),
  1726.             'bash crashed with signal 42 in foo()')
  1727.  
  1728.         # do not crash on empty StacktraceTop
  1729.         report['StacktraceTop'] = ''
  1730.         self.assertEqual(report.standard_title(),
  1731.             'bash crashed with signal 42')
  1732.  
  1733.         # do not create bug title with unknown function name
  1734.         report['StacktraceTop'] = '??()\nfoo()'
  1735.         self.assertEqual(report.standard_title(),
  1736.             'bash crashed with signal 42 in foo()')
  1737.  
  1738.         # if we do not know any function name, don't mention ??
  1739.         report['StacktraceTop'] = '??()\n??()'
  1740.         self.assertEqual(report.standard_title(),
  1741.             'bash crashed with signal 42')
  1742.  
  1743.         # Python crash
  1744.         report = Report()
  1745.         report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
  1746.         report['Traceback'] = '''Traceback (most recent call last):
  1747. File "/usr/share/apport/apport-gtk", line 202, in <module>
  1748. app.run_argv()
  1749. File "/var/lib/python-support/python2.5/apport/ui.py", line 161, in run_argv
  1750. self.run_crashes()
  1751. File "/var/lib/python-support/python2.5/apport/ui.py", line 104, in run_crashes
  1752. self.run_crash(f)
  1753. File "/var/lib/python-support/python2.5/apport/ui.py", line 115, in run_crash
  1754. response = self.ui_present_crash(desktop_entry)
  1755. File "/usr/share/apport/apport-gtk", line 67, in ui_present_crash
  1756. subprocess.call(['pgrep', '-x',
  1757. NameError: global name 'subprocess' is not defined'''
  1758.         self.assertEqual(report.standard_title(),
  1759.             'apport-gtk crashed with NameError in ui_present_crash()')
  1760.  
  1761.         # slightly weird Python crash
  1762.         report = Report()
  1763.         report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
  1764.         report['Traceback'] = '''TypeError: Cannot create a consistent method resolution
  1765. order (MRO) for bases GObject, CanvasGroupableIface, CanvasGroupable'''
  1766.         self.assertEqual(report.standard_title(),
  1767.             'apport-gtk crashed with TypeError: Cannot create a consistent method resolution')
  1768.  
  1769.         # Python crash with custom message
  1770.         report = Report()
  1771.         report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
  1772.         report['Traceback'] = '''Traceback (most recent call last):
  1773.   File "/x/foo.py", line 242, in setup_chooser
  1774.     raise "Moo"
  1775. Moo'''
  1776.  
  1777.         self.assertEqual(report.standard_title(), 'apport-gtk crashed with Moo in setup_chooser()')
  1778.  
  1779.         # Python crash with custom message with newlines (LP #190947)
  1780.         report = Report()
  1781.         report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
  1782.         report['Traceback'] = '''Traceback (most recent call last):
  1783.   File "/x/foo.py", line 242, in setup_chooser
  1784.     raise "\nKey: "+key+" isn't set.\nRestarting AWN usually solves this issue\n"
  1785.  
  1786. Key: /apps/avant-window-navigator/app/active_png isn't set.
  1787. Restarting AWN usually solves this issue'''
  1788.  
  1789.         t = report.standard_title()
  1790.         self.assert_(t.startswith('apport-gtk crashed with'))
  1791.         self.assert_(t.endswith('setup_chooser()'))
  1792.  
  1793.         # package install problem
  1794.         report = Report('Package')
  1795.         report['Package'] = 'bash'
  1796.  
  1797.         # no ErrorMessage
  1798.         self.assertEqual(report.standard_title(),
  1799.             'package bash failed to install/upgrade')
  1800.  
  1801.         # empty ErrorMessage
  1802.         report['ErrorMessage'] = ''
  1803.         self.assertEqual(report.standard_title(),
  1804.             'package bash failed to install/upgrade')
  1805.  
  1806.         # nonempty ErrorMessage
  1807.         report['ErrorMessage'] = 'botched\nnot found\n'
  1808.         self.assertEqual(report.standard_title(),
  1809.             'package bash failed to install/upgrade: not found')
  1810.  
  1811.         # matching package/system architectures
  1812.         report['Signal'] = '11'
  1813.         report['ExecutablePath'] = '/bin/bash'
  1814.         report['StacktraceTop'] = '''foo()
  1815. bar(x=3)
  1816. baz()
  1817. '''
  1818.         report['PackageArchitecture'] = 'amd64'
  1819.         report['Architecture'] = 'amd64'
  1820.         self.assertEqual(report.standard_title(),
  1821.             'bash crashed with SIGSEGV in foo()')
  1822.  
  1823.         # non-native package (on multiarch)
  1824.         report['PackageArchitecture'] = 'i386'
  1825.         self.assertEqual(report.standard_title(),
  1826.             'bash crashed with SIGSEGV in foo() [non-native i386 package]')
  1827.  
  1828.         # Arch: all package (matches every system architecture)
  1829.         report['PackageArchitecture'] = 'all'
  1830.         self.assertEqual(report.standard_title(),
  1831.             'bash crashed with SIGSEGV in foo()')
  1832.  
  1833.         report = Report('KernelOops')
  1834.         report['OopsText'] = '------------[ cut here ]------------\nkernel BUG at /tmp/oops.c:5!\ninvalid opcode: 0000 [#1] SMP'
  1835.         self.assertEqual(report.standard_title(),'kernel BUG at /tmp/oops.c:5!')
  1836.  
  1837.     def test_obsolete_packages(self):
  1838.         '''obsolete_packages().'''
  1839.  
  1840.         report = Report()
  1841.         self.assertRaises(KeyError, report.obsolete_packages)
  1842.  
  1843.         # should work without Dependencies
  1844.         report['Package'] = 'bash 0'
  1845.         self.assertEqual(report.obsolete_packages(), ['bash'])
  1846.         report['Package'] = 'bash 0 [modified: /bin/bash]'
  1847.         self.assertEqual(report.obsolete_packages(), ['bash'])
  1848.         report['Package'] = 'bash ' + packaging.get_available_version('bash')
  1849.         self.assertEqual(report.obsolete_packages(), [])
  1850.  
  1851.         report['Dependencies'] = 'coreutils 0\ncron 0\n'
  1852.         self.assertEqual(report.obsolete_packages(), ['coreutils', 'cron'])
  1853.  
  1854.         report['Dependencies'] = 'coreutils %s [modified: /bin/mount]\ncron 0\n' % \
  1855.             packaging.get_available_version('coreutils')
  1856.         self.assertEqual(report.obsolete_packages(), ['cron'])
  1857.  
  1858.         report['Dependencies'] = 'coreutils %s\ncron %s\n' % (
  1859.             packaging.get_available_version('coreutils'),
  1860.             packaging.get_available_version('cron'))
  1861.         self.assertEqual(report.obsolete_packages(), [])
  1862.  
  1863.     def test_gen_stacktrace_top(self):
  1864.         '''_gen_stacktrace_top().'''
  1865.         
  1866.         # nothing to chop off
  1867.         r = Report()
  1868.         r['Stacktrace'] = '''#0  0x10000488 in h (p=0x0) at crash.c:25
  1869. #1  0x100004c8 in g (x=1, y=42) at crash.c:26
  1870. #2  0x10000514 in f (x=1) at crash.c:27
  1871. #3  0x10000530 in e (x=1) at crash.c:28
  1872. #4  0x10000530 in d (x=1) at crash.c:29
  1873. #5  0x10000530 in c (x=1) at crash.c:30
  1874. #6  0x10000550 in main () at crash.c:31
  1875. '''
  1876.         r._gen_stacktrace_top()
  1877.         self.assertEqual(r['StacktraceTop'], '''h (p=0x0) at crash.c:25
  1878. g (x=1, y=42) at crash.c:26
  1879. f (x=1) at crash.c:27
  1880. e (x=1) at crash.c:28
  1881. d (x=1) at crash.c:29''')
  1882.  
  1883.         # nothing to chop off: some addresses missing (LP #269133)
  1884.         r = Report()
  1885.         r['Stacktrace'] = '''#0 h (p=0x0) at crash.c:25
  1886. #1  0x100004c8 in g (x=1, y=42) at crash.c:26
  1887. #2 f (x=1) at crash.c:27
  1888. #3  0x10000530 in e (x=1) at crash.c:28
  1889. #4  0x10000530 in d (x=1) at crash.c:29
  1890. #5  0x10000530 in c (x=1) at crash.c:30
  1891. #6  0x10000550 in main () at crash.c:31
  1892. '''
  1893.         r._gen_stacktrace_top()
  1894.         self.assertEqual(r['StacktraceTop'], '''h (p=0x0) at crash.c:25
  1895. g (x=1, y=42) at crash.c:26
  1896. f (x=1) at crash.c:27
  1897. e (x=1) at crash.c:28
  1898. d (x=1) at crash.c:29''')
  1899.  
  1900.         # single signal handler invocation
  1901.         r = Report()
  1902.         r['Stacktrace'] = '''#0  0x10000488 in raise () from /lib/libpthread.so.0
  1903. #1  0x100004c8 in ??
  1904. #2  <signal handler called>
  1905. #3  0x10000530 in e (x=1) at crash.c:28
  1906. #4  0x10000530 in d (x=1) at crash.c:29
  1907. #5  0x10000530 in c (x=1) at crash.c:30
  1908. #6  0x10000550 in main () at crash.c:31
  1909. '''
  1910.         r._gen_stacktrace_top()
  1911.         self.assertEqual(r['StacktraceTop'], '''e (x=1) at crash.c:28
  1912. d (x=1) at crash.c:29
  1913. c (x=1) at crash.c:30
  1914. main () at crash.c:31''')
  1915.  
  1916.         # single signal handler invocation: some addresses missing
  1917.         r = Report()
  1918.         r['Stacktrace'] = '''#0  0x10000488 in raise () from /lib/libpthread.so.0
  1919. #1  ??
  1920. #2  <signal handler called>
  1921. #3  0x10000530 in e (x=1) at crash.c:28
  1922. #4  d (x=1) at crash.c:29
  1923. #5  0x10000530 in c (x=1) at crash.c:30
  1924. #6  0x10000550 in main () at crash.c:31
  1925. '''
  1926.         r._gen_stacktrace_top()
  1927.         self.assertEqual(r['StacktraceTop'], '''e (x=1) at crash.c:28
  1928. d (x=1) at crash.c:29
  1929. c (x=1) at crash.c:30
  1930. main () at crash.c:31''')
  1931.  
  1932.         # stacked signal handler; should only cut the first one
  1933.         r = Report()
  1934.         r['Stacktrace'] = '''#0  0x10000488 in raise () from /lib/libpthread.so.0
  1935. #1  0x100004c8 in ??
  1936. #2  <signal handler called>
  1937. #3  0x10000530 in e (x=1) at crash.c:28
  1938. #4  0x10000530 in d (x=1) at crash.c:29
  1939. #5  0x10000123 in raise () from /lib/libpthread.so.0
  1940. #6  <signal handler called>
  1941. #7  0x10000530 in c (x=1) at crash.c:30
  1942. #8  0x10000550 in main () at crash.c:31
  1943. '''
  1944.         r._gen_stacktrace_top()
  1945.         self.assertEqual(r['StacktraceTop'], '''e (x=1) at crash.c:28
  1946. d (x=1) at crash.c:29
  1947. raise () from /lib/libpthread.so.0
  1948. <signal handler called>
  1949. c (x=1) at crash.c:30''')
  1950.  
  1951.         # Gnome assertion; should unwind the logs and assert call
  1952.         r = Report()
  1953.         r['Stacktrace'] = '''#0  0xb7d39cab in IA__g_logv (log_domain=<value optimized out>, log_level=G_LOG_LEVEL_ERROR, 
  1954.     format=0xb7d825f0 "file %s: line %d (%s): assertion failed: (%s)", args1=0xbfee8e3c "xxx") at /build/buildd/glib2.0-2.13.5/glib/gmessages.c:493
  1955. #1  0xb7d39f29 in IA__g_log (log_domain=0xb7edbfd0 "libgnomevfs", log_level=G_LOG_LEVEL_ERROR, 
  1956.     format=0xb7d825f0 "file %s: line %d (%s): assertion failed: (%s)") at /build/buildd/glib2.0-2.13.5/glib/gmessages.c:517
  1957. #2  0xb7d39fa6 in IA__g_assert_warning (log_domain=0xb7edbfd0 "libgnomevfs", file=0xb7ee1a26 "gnome-vfs-volume.c", line=254, 
  1958.     pretty_function=0xb7ee1920 "gnome_vfs_volume_unset_drive_private", expression=0xb7ee1a39 "volume->priv->drive == drive")
  1959.     at /build/buildd/glib2.0-2.13.5/glib/gmessages.c:552
  1960. No locals.
  1961. #3  0xb7ec6c11 in gnome_vfs_volume_unset_drive_private (volume=0x8081a30, drive=0x8078f00) at gnome-vfs-volume.c:254
  1962.         __PRETTY_FUNCTION__ = "gnome_vfs_volume_unset_drive_private"
  1963. #4  0x08054db8 in _gnome_vfs_volume_monitor_disconnected (volume_monitor=0x8070400, drive=0x8078f00) at gnome-vfs-volume-monitor.c:963
  1964.         vol_list = (GList *) 0x8096d30
  1965.         current_vol = (GList *) 0x8097470
  1966. #5  0x0805951e in _hal_device_removed (hal_ctx=0x8074da8, udi=0x8093be4 "/org/freedesktop/Hal/devices/volume_uuid_92FC9DFBFC9DDA35")
  1967.     at gnome-vfs-hal-mounts.c:1316
  1968.         backing_udi = <value optimized out>
  1969. #6  0xb7ef1ead in filter_func (connection=0x8075288, message=0x80768d8, user_data=0x8074da8) at libhal.c:820
  1970.         udi = <value optimized out>
  1971.         object_path = 0x8076d40 "/org/freedesktop/Hal/Manager"
  1972.         error = {name = 0x0, message = 0x0, dummy1 = 1, dummy2 = 0, dummy3 = 0, dummy4 = 1, dummy5 = 0, padding1 = 0xb7e50c00}
  1973. #7  0xb7e071d2 in dbus_connection_dispatch (connection=0x8075288) at dbus-connection.c:4267
  1974. #8  0xb7e33dfd in ?? () from /usr/lib/libdbus-glib-1.so.2'''
  1975.         r._gen_stacktrace_top()
  1976.         self.assertEqual(r['StacktraceTop'], '''gnome_vfs_volume_unset_drive_private (volume=0x8081a30, drive=0x8078f00) at gnome-vfs-volume.c:254
  1977. _gnome_vfs_volume_monitor_disconnected (volume_monitor=0x8070400, drive=0x8078f00) at gnome-vfs-volume-monitor.c:963
  1978. _hal_device_removed (hal_ctx=0x8074da8, udi=0x8093be4 "/org/freedesktop/Hal/devices/volume_uuid_92FC9DFBFC9DDA35")
  1979. filter_func (connection=0x8075288, message=0x80768d8, user_data=0x8074da8) at libhal.c:820
  1980. dbus_connection_dispatch (connection=0x8075288) at dbus-connection.c:4267''')
  1981.  
  1982.     def test_crash_signature(self):
  1983.         '''crash_signature().'''
  1984.  
  1985.         r = Report()
  1986.         self.assertEqual(r.crash_signature(), None)
  1987.  
  1988.         # signal crashes
  1989.         r['Signal'] = '42'
  1990.         r['ExecutablePath'] = '/bin/crash'
  1991.  
  1992.         r['StacktraceTop'] = '''foo_bar (x=1) at crash.c:28
  1993. d01 (x=1) at crash.c:29
  1994. raise () from /lib/libpthread.so.0
  1995. <signal handler called>
  1996. __frob::~frob (x=1) at crash.c:30'''
  1997.  
  1998.         self.assertEqual(r.crash_signature(), '/bin/crash:42:foo_bar:d01:raise:<signal handler called>:__frob::~frob')
  1999.  
  2000.         r['StacktraceTop'] = '''foo_bar (x=1) at crash.c:28
  2001. ??
  2002. raise () from /lib/libpthread.so.0
  2003. <signal handler called>
  2004. __frob (x=1) at crash.c:30'''
  2005.         self.assertEqual(r.crash_signature(), None)
  2006.  
  2007.         # Python crashes
  2008.         del r['Signal']
  2009.         r['Traceback'] = '''Traceback (most recent call last):
  2010.   File "test.py", line 7, in <module>
  2011.     print _f(5)
  2012.   File "test.py", line 5, in _f
  2013.     return g_foo00(x+1)
  2014.   File "test.py", line 2, in g_foo00
  2015.     return x/0
  2016. ZeroDivisionError: integer division or modulo by zero'''
  2017.         self.assertEqual(r.crash_signature(), '/bin/crash:ZeroDivisionError:<module>:_f:g_foo00')
  2018.  
  2019.         # sometimes Python traces do not have file references
  2020.         r['Traceback'] = 'TypeError: function takes exactly 0 arguments (1 given)'
  2021.         self.assertEqual(r.crash_signature(), '/bin/crash:TypeError')
  2022.  
  2023.         r['Traceback'] = 'FooBar'
  2024.         self.assertEqual(r.crash_signature(), None)
  2025.  
  2026.     def test_binary_data(self):
  2027.         '''methods get along with binary data.'''
  2028.  
  2029.         pr = Report()
  2030.         pr['Signal'] = '11'
  2031.         pr['ExecutablePath'] = '/bin/foo'
  2032.         pr['Stacktrace'] = '''#0  0x10000488 in h (p="\0\0\0\1\2") at crash.c:25
  2033. #1  0x10000550 in main () at crash.c:31
  2034. '''
  2035.         pr['ThreadStacktrace'] = pr['Stacktrace']
  2036.         pr['ProcCmdline'] = 'python\0-OO\011\0/bin/bash'
  2037.         pr._gen_stacktrace_top()
  2038.  
  2039.         io = StringIO()
  2040.         pr.write(io)
  2041.         io.seek(0)
  2042.         pr = Report()
  2043.         pr.load(io, binary='compressed')
  2044.  
  2045.         assert hasattr(pr['StacktraceTop'], 'get_value')
  2046.  
  2047.         self.assertEqual(pr.has_useful_stacktrace(), True)
  2048.         self.assertEqual(pr.crash_signature(), '/bin/foo:11:h:main')
  2049.         self.assertEqual(pr.standard_title(), 'foo crashed with SIGSEGV in h()')
  2050.  
  2051.     def test_module_license_evaluation(self):
  2052.         '''module licenses can be validated correctly.'''
  2053.  
  2054.         def _build_ko(license):
  2055.             asm = tempfile.NamedTemporaryFile(prefix='%s-' % (license),
  2056.                                               suffix='.S')
  2057.             asm.write('.section .modinfo\n.string "license=%s"\n' % (license))
  2058.             asm.flush()
  2059.             ko = tempfile.NamedTemporaryFile(prefix='%s-' % (license),
  2060.                                              suffix='.ko')
  2061.             subprocess.call(['/usr/bin/as',asm.name,'-o',ko.name])
  2062.             return ko
  2063.         
  2064.         good_ko = _build_ko('GPL')
  2065.         bad_ko  = _build_ko('BAD')
  2066.  
  2067.         # test:
  2068.         #  - loaded real module
  2069.         #  - unfindable module
  2070.         #  - fake GPL module
  2071.         #  - fake BAD module
  2072.  
  2073.         # direct license check
  2074.         self.assert_('GPL' in get_module_license('isofs'))
  2075.         self.assert_(get_module_license('does-not-exist') == None)
  2076.         self.assert_('GPL' in get_module_license(good_ko.name))
  2077.         self.assert_('BAD' in get_module_license(bad_ko.name))
  2078.  
  2079.         # check via nonfree_modules logic
  2080.         f = tempfile.NamedTemporaryFile()
  2081.         f.write('isofs\ndoes-not-exist\n%s\n%s\n' %
  2082.                 (good_ko.name,bad_ko.name))
  2083.         f.flush()
  2084.         nonfree = nonfree_modules(f.name)
  2085.         self.failIf('isofs' in nonfree)
  2086.         self.failIf('does-not-exist' in nonfree)
  2087.         self.failIf(good_ko.name in nonfree)
  2088.         self.assert_(bad_ko.name in nonfree)
  2089.  
  2090. if __name__ == '__main__':
  2091.     unittest.main()
  2092.